home *** CD-ROM | disk | FTP | other *** search
/ PC World 2008 September / PCWorld_2008-09_cd.bin / v cisle / sadanastroju / lightning-0.8-tb-win.xpi / js / calICSCalendar.js < prev    next >
Text File  |  2008-02-25  |  35KB  |  936 lines

  1. /* ***** BEGIN LICENSE BLOCK *****
  2.  * Version: MPL 1.1/GPL 2.0/LGPL 2.1
  3.  *
  4.  * The contents of this file are subject to the Mozilla Public License Version
  5.  * 1.1 (the "License"); you may not use this file except in compliance with
  6.  * the License. You may obtain a copy of the License at
  7.  * http://www.mozilla.org/MPL/
  8.  *
  9.  * Software distributed under the License is distributed on an "AS IS" basis,
  10.  * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
  11.  * for the specific language governing rights and limitations under the
  12.  * License.
  13.  *
  14.  * The Original Code is mozilla calendar code.
  15.  *
  16.  * The Initial Developer of the Original Code is
  17.  *   Michiel van Leeuwen <mvl@exedo.nl>
  18.  * Portions created by the Initial Developer are Copyright (C) 2004
  19.  * the Initial Developer. All Rights Reserved.
  20.  *
  21.  * Contributor(s):
  22.  *   Vladimir Vukicevic <vladimir.vukicevic@oracle.com>
  23.  *   Dan Mosedale <dan.mosedale@oracle.com>
  24.  *   Joey Minta <jminta@gmail.com>
  25.  *   Philipp Kewisch <mozilla@kewis.ch>
  26.  *
  27.  * Alternatively, the contents of this file may be used under the terms of
  28.  * either the GNU General Public License Version 2 or later (the "GPL"), or
  29.  * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
  30.  * in which case the provisions of the GPL or the LGPL are applicable instead
  31.  * of those above. If you wish to allow use of your version of this file only
  32.  * under the terms of either the GPL or the LGPL, and not to allow others to
  33.  * use your version of this file under the terms of the MPL, indicate your
  34.  * decision by deleting the provisions above and replace them with the notice
  35.  * and other provisions required by the GPL or the LGPL. If you do not delete
  36.  * the provisions above, a recipient may use your version of this file under
  37.  * the terms of any one of the MPL, the GPL or the LGPL.
  38.  *
  39.  * ***** END LICENSE BLOCK ***** */
  40.  
  41. //
  42. // calICSCalendar.js
  43. //
  44. // This is a non-sync ics file. It reads the file pointer to by uri when set,
  45. // then writes it on updates. External changes to the file will be
  46. // ignored and overwritten.
  47. //
  48. // XXX Should do locks, so that external changes are not overwritten.
  49.  
  50. const CI = Components.interfaces;
  51. const calIOperationListener = Components.interfaces.calIOperationListener;
  52. const calICalendar = Components.interfaces.calICalendar;
  53. const calIErrors = Components.interfaces.calIErrors;
  54.  
  55. var appInfo = Components.classes["@mozilla.org/xre/app-info;1"].
  56.                          getService(Components.interfaces.nsIXULAppInfo);
  57. var isOnBranch = appInfo.platformVersion.indexOf("1.8") == 0;
  58.  
  59. function calICSCalendar() {
  60.     this.initProviderBase();
  61.     this.initICSCalendar();
  62.  
  63.     this.unmappedComponents = [];
  64.     this.unmappedProperties = [];
  65.     this.queue = new Array();
  66. }
  67.  
  68. calICSCalendar.prototype = {
  69.     __proto__: calProviderBase.prototype,
  70.  
  71.     mObserver: null,
  72.     locked: false,
  73.  
  74.     QueryInterface: function (aIID) {
  75.         return doQueryInterface(this, calICSCalendar.prototype, aIID,
  76.                                 [Components.interfaces.calICalendarProvider,
  77.                                  Components.interfaces.nsIStreamListener,
  78.                                  Components.interfaces.nsIStreamLoaderObserver,
  79.                                  Components.interfaces.nsIInterfaceRequestor]);
  80.     },
  81.     
  82.     initICSCalendar: function() {
  83.         this.mMemoryCalendar = Components.classes["@mozilla.org/calendar/calendar;1?type=memory"]
  84.                                          .createInstance(Components.interfaces.calICalendar);
  85.  
  86.         this.mMemoryCalendar.superCalendar = this;
  87.         this.mObserver = new calICSObserver(this);
  88.         this.mMemoryCalendar.addObserver(this.mObserver); // XXX Not removed
  89.     },
  90.  
  91.     //
  92.     // calICalendarProvider interface
  93.     //
  94.     get prefChromeOverlay() {
  95.         return null;
  96.     },
  97.  
  98.     get displayName() {
  99.         return calGetString("calendar", "icsName");
  100.     },
  101.  
  102.     createCalendar: function ics_createCal() {
  103.         throw NS_ERROR_NOT_IMPLEMENTED;
  104.     },
  105.  
  106.     deleteCalendar: function ics_deleteCal(cal, listener) {
  107.         throw NS_ERROR_NOT_IMPLEMENTED;
  108.     },
  109.  
  110.     //
  111.     // calICalendar interface
  112.     //
  113.     get type() { return "ics"; },
  114.  
  115.     get canRefresh() {
  116.         return true;
  117.     },
  118.  
  119.     get uri() { return this.mUri },
  120.     set uri(aUri) {
  121.         this.mUri = aUri;
  122.         this.mMemoryCalendar.uri = this.mUri;
  123.  
  124.         // Use the ioservice, to create a channel, which makes finding the
  125.         // right hooks to use easier.
  126.         var ioService = Components.classes["@mozilla.org/network/io-service;1"]
  127.                                   .getService(Components.interfaces.nsIIOService);
  128.         var channel = ioService.newChannelFromURI(this.mUri);
  129.  
  130.         if (channel instanceof Components.interfaces.nsIHttpChannel) {
  131.             this.mHooks = new httpHooks();
  132.         } else {
  133.             this.mHooks = new dummyHooks();
  134.         }
  135.  
  136.         this.refresh();
  137.     },
  138.  
  139.     getProperty: function calICSCalendar_getProperty(aName) {
  140.         switch (aName) {
  141.             case "requiresNetwork":
  142.                 return (!this.uri.schemeIs("file"));
  143.         }
  144.         return this.__proto__.__proto__.getProperty.apply(this, arguments);
  145.     },
  146.  
  147.     refresh: function calICSCalendar_refresh() {
  148.         this.queue.push({action: 'refresh'});
  149.         this.processQueue();
  150.     },
  151.  
  152.     doRefresh: function calICSCalendar_doRefresh() {
  153.         var ioService = Components.classes["@mozilla.org/network/io-service;1"]
  154.                                   .getService(Components.interfaces.nsIIOService);
  155.  
  156.         var channel = ioService.newChannelFromURI(this.mUri);
  157.         channel.loadFlags |= Components.interfaces.nsIRequest.LOAD_BYPASS_CACHE;
  158.         channel.notificationCallbacks = this;
  159.  
  160.         // Allow the hook to do its work, like a performing a quick check to
  161.         // see if the remote file really changed. Might save a lot of time
  162.         this.mHooks.onBeforeGet(channel);
  163.  
  164.         var streamLoader = Components.classes["@mozilla.org/network/stream-loader;1"]
  165.                                      .createInstance(Components.interfaces.nsIStreamLoader);
  166.  
  167.         // Lock other changes to the item list.
  168.         this.lock();
  169.  
  170.         try {
  171.             if (isOnBranch) {
  172.                 streamLoader.init(channel, this, this);
  173.             } else {
  174.                 streamLoader.init(this);
  175.                 channel.asyncOpen(streamLoader, this);
  176.             }
  177.         } catch(e) {
  178.             // File not found: a new calendar. No problem.
  179.             this.unlock();
  180.         }
  181.     },
  182.  
  183.     calendarPromotedProps: {
  184.         "PRODID": true,
  185.         "VERSION": true
  186.     },
  187.  
  188.     // nsIStreamLoaderObserver impl
  189.     // Listener for download. Parse the downloaded file
  190.  
  191.     onStreamComplete: function(loader, ctxt, status, resultLength, result)
  192.     {
  193.         // No need to do anything if there was no result
  194.         if (!resultLength) {
  195.             this.unlock();
  196.             return;
  197.         }
  198.         
  199.         // Allow the hook to get needed data (like an etag) of the channel
  200.         var cont = this.mHooks.onAfterGet();
  201.         if (!cont) {
  202.             this.unlock();
  203.             return;
  204.         }
  205.  
  206.         // This conversion is needed, because the stream only knows about
  207.         // byte arrays, not about strings or encodings. The array of bytes
  208.         // need to be interpreted as utf8 and put into a javascript string.
  209.         var unicodeConverter = Components.classes["@mozilla.org/intl/scriptableunicodeconverter"]
  210.                                          .createInstance(Components.interfaces.nsIScriptableUnicodeConverter);
  211.         // ics files are always utf8
  212.         unicodeConverter.charset = "UTF-8";
  213.         var str;
  214.         try {
  215.             str = unicodeConverter.convertFromByteArray(result, result.length);
  216.         } catch(e) {
  217.             this.mObserver.onError(calIErrors.CAL_UTF8_DECODING_FAILED, e.toString());
  218.             this.unlock();
  219.             return;
  220.         }
  221.  
  222.         // Create a new calendar, to get rid of all the old events
  223.         // Don't forget to remove the observer
  224.         this.mMemoryCalendar.removeObserver(this.mObserver);
  225.         this.mMemoryCalendar = Components.classes["@mozilla.org/calendar/calendar;1?type=memory"]
  226.                                          .createInstance(Components.interfaces.calICalendar);
  227.         this.mMemoryCalendar.uri = this.mUri;
  228.         this.mMemoryCalendar.superCalendar = this;
  229.  
  230.         this.mObserver.onStartBatch();
  231.  
  232.         // Wrap parsing in a try block. Will ignore errors. That's a good thing
  233.         // for non-existing or empty files, but not good for invalid files.
  234.         // That's why we put them in readOnly mode
  235.         try {
  236.             var parser = Components.classes["@mozilla.org/calendar/ics-parser;1"].
  237.                                     createInstance(Components.interfaces.calIIcsParser);
  238.             parser.parseString(str, null);
  239.             var items = parser.getItems({});
  240.             
  241.             for each (var item in items) {
  242.                 this.mMemoryCalendar.adoptItem(item, null);
  243.             }
  244.             this.unmappedComponents = parser.getComponents({});
  245.             this.unmappedProperties = parser.getProperties({});
  246.         } catch(e) {
  247.             LOG("Parsing the file failed:"+e);
  248.             this.mObserver.onError(e.result, e.toString());
  249.         }
  250.         this.mObserver.onEndBatch();
  251.         this.mObserver.onLoad(this);
  252.         
  253.         // Now that all items have been stuffed into the memory calendar
  254.         // we should add ourselves as observer. It is important that this
  255.         // happens *after* the calls to adoptItem in the above loop to prevent
  256.         // the views from being notified.
  257.         this.mMemoryCalendar.addObserver(this.mObserver);
  258.         
  259.         this.unlock();
  260.     },
  261.  
  262.     writeICS: function () {
  263.         this.lock();
  264.         try {
  265.             if (!this.mUri)
  266.                 throw Components.results.NS_ERROR_FAILURE;
  267.             // makeBackup will call doWriteICS
  268.             this.makeBackup(this.doWriteICS);
  269.         } catch (exc) {
  270.             this.unlock();
  271.             throw exc;
  272.         }
  273.     },
  274.  
  275.     doWriteICS: function () {
  276.         var savedthis = this;
  277.         var appStartup = Components.classes["@mozilla.org/toolkit/app-startup;1"]
  278.                                    .getService(Components.interfaces.nsIAppStartup);
  279.         var listener =
  280.         {
  281.             serializer: null,
  282.             onOperationComplete: function(aCalendar, aStatus, aOperationType, aId, aDetail)
  283.             {
  284.                 var inLastWindowClosingSurvivalArea = false;
  285.                 try  {
  286.                     // All events are returned. Now set up a channel and a
  287.                     // streamloader to upload.  onStopRequest will be called
  288.                     // once the write has finished
  289.                     var ioService = Components.classes
  290.                         ["@mozilla.org/network/io-service;1"]
  291.                         .getService(Components.interfaces.nsIIOService);
  292.                     var channel = ioService.newChannelFromURI(savedthis.mUri);
  293.  
  294.                     // Allow the hook to add things to the channel, like a
  295.                     // header that checks etags
  296.                     savedthis.mHooks.onBeforePut(channel);
  297.  
  298.                     channel.notificationCallbacks = savedthis;
  299.                     var uploadChannel = channel.QueryInterface(
  300.                         Components.interfaces.nsIUploadChannel);
  301.  
  302.                     // Serialize
  303.                     var icsStream = this.serializer.serializeToInputStream();
  304.  
  305.                     // Upload
  306.                     uploadChannel.setUploadStream(icsStream,
  307.                                                   "text/calendar", -1);
  308.  
  309.                     appStartup.enterLastWindowClosingSurvivalArea();
  310.                     inLastWindowClosingSurvivalArea = true;
  311.                     channel.asyncOpen(savedthis, savedthis);
  312.                 } catch (ex) {
  313.                     if (inLastWindowClosingSurvivalArea) {
  314.                         appStartup.exitLastWindowClosingSurvivalArea();
  315.                     }
  316.                     savedthis.mObserver.onError(
  317.                         ex.result, "The calendar could not be saved; there " +
  318.                         "was a failure: 0x" + ex.result.toString(16));
  319.                     savedthis.unlock();
  320.                 }
  321.             },
  322.             onGetResult: function(aCalendar, aStatus, aItemType, aDetail, aCount, aItems)
  323.             {
  324.                 this.serializer.addItems(aItems, aCount);
  325.             }
  326.         };
  327.         listener.serializer = Components.classes["@mozilla.org/calendar/ics-serializer;1"].
  328.                                          createInstance(Components.interfaces.calIIcsSerializer);
  329.         for each (var comp in this.unmappedComponents) {
  330.             listener.serializer.addComponent(comp);
  331.         }
  332.         for each (var prop in this.unmappedProperties) {
  333.             listener.serializer.addProperty(prop);
  334.         }
  335.  
  336.         // don't call this.getItems, because we are locked:
  337.         this.mMemoryCalendar.getItems(calICalendar.ITEM_FILTER_TYPE_ALL | calICalendar.ITEM_FILTER_COMPLETED_ALL,
  338.                                       0, null, null, listener);
  339.     },
  340.  
  341.     // nsIStreamListener impl
  342.     // For after publishing. Do error checks here
  343.     onStartRequest: function(request, ctxt) {},
  344.     onDataAvailable: function(request, ctxt, inStream, sourceOffset, count) {
  345.          // All data must be consumed. For an upload channel, there is
  346.          // no meaningfull data. So it gets read and then ignored
  347.          var scriptableInputStream = 
  348.              Components.classes["@mozilla.org/scriptableinputstream;1"]
  349.                        .createInstance(Components.interfaces.nsIScriptableInputStream);
  350.          scriptableInputStream.init(inStream);
  351.          scriptableInputStream.read(-1);
  352.     },
  353.     onStopRequest: function(request, ctxt, status, errorMsg)
  354.     {
  355.         ctxt = ctxt.wrappedJSObject;
  356.         var channel;
  357.         try {
  358.             channel = request.QueryInterface(Components.interfaces.nsIHttpChannel);
  359.             LOG("calICSCalendar channel.requestSucceeded: " + channel.requestSucceeded);
  360.         } catch(e) {
  361.         }
  362.  
  363.         if (channel && !channel.requestSucceeded) {
  364.             ctxt.mObserver.onError(channel.requestSucceeded,
  365.                                    "Publishing the calendar file failed\n" +
  366.                                        "Status code: "+channel.responseStatus+": "+channel.responseStatusText+"\n");
  367.         }
  368.  
  369.         else if (!channel && !Components.isSuccessCode(request.status)) {
  370.             ctxt.mObserver.onError(request.status,
  371.                                    "Publishing the calendar file failed\n" +
  372.                                        "Status code: "+request.status.toString(16)+"\n");
  373.         }
  374.  
  375.         // Allow the hook to grab data of the channel, like the new etag
  376.         ctxt.mHooks.onAfterPut(channel);
  377.  
  378.         ctxt.unlock();
  379.         var appStartup = Components.classes["@mozilla.org/toolkit/app-startup;1"]
  380.                                    .getService(Components.interfaces.nsIAppStartup);
  381.         appStartup.exitLastWindowClosingSurvivalArea();
  382.     },
  383.  
  384.     // Always use the queue, just to reduce the amount of places where
  385.     // this.mMemoryCalendar.addItem() and friends are called. less
  386.     // copied code.
  387.     addItem: function (aItem, aListener) {
  388.         this.adoptItem(aItem.clone(), aListener);
  389.     },
  390.     adoptItem: function (aItem, aListener) {
  391.         if (this.readOnly) 
  392.             throw Components.interfaces.calIErrors.CAL_IS_READONLY;
  393.         this.queue.push({action:'add', item:aItem, listener:aListener});
  394.         this.processQueue();
  395.     },
  396.  
  397.     modifyItem: function (aNewItem, aOldItem, aListener) {
  398.         if (this.readOnly) 
  399.             throw Components.interfaces.calIErrors.CAL_IS_READONLY;
  400.         this.queue.push({action:'modify', oldItem: aOldItem,
  401.                          newItem: aNewItem, listener:aListener});
  402.         this.processQueue();
  403.     },
  404.  
  405.     deleteItem: function (aItem, aListener) {
  406.         if (this.readOnly) 
  407.             throw Components.interfaces.calIErrors.CAL_IS_READONLY;
  408.         this.queue.push({action:'delete', item:aItem, listener:aListener});
  409.         this.processQueue();
  410.     },
  411.  
  412.     getItem: function (aId, aListener) {
  413.         this.queue.push({action:'get_item', id:aId, listener:aListener});
  414.         this.processQueue();
  415.     },
  416.  
  417.     getItems: function (aItemFilter, aCount,
  418.                         aRangeStart, aRangeEnd, aListener)
  419.     {
  420.         this.queue.push({action:'get_items',
  421.                          itemFilter:aItemFilter, count:aCount,
  422.                          rangeStart:aRangeStart, rangeEnd:aRangeEnd,
  423.                          listener:aListener});
  424.         this.processQueue();
  425.     },
  426.  
  427.     processQueue: function ()
  428.     {
  429.         if (this.isLocked())
  430.             return;
  431.         var a;
  432.         var writeICS = false;
  433.         var refreshAction = null;
  434.         while ((a = this.queue.shift())) {
  435.             switch (a.action) {
  436.                 case 'add':
  437.                     this.mMemoryCalendar.addItem(a.item, a.listener);
  438.                     writeICS = true;
  439.                     break;
  440.                 case 'modify':
  441.                     this.mMemoryCalendar.modifyItem(a.newItem, a.oldItem,
  442.                                                     a.listener);
  443.                     writeICS = true;
  444.                     break;
  445.                 case 'delete':
  446.                     this.mMemoryCalendar.deleteItem(a.item, a.listener);
  447.                     writeICS = true;
  448.                     break;
  449.                 case 'get_item':
  450.                     this.mMemoryCalendar.getItem(a.id, a.listener);
  451.                     break;
  452.                 case 'get_items':
  453.                     this.mMemoryCalendar.getItems(a.itemFilter, a.count,
  454.                                                   a.rangeStart, a.rangeEnd,
  455.                                                   a.listener);
  456.                     break;
  457.                 case 'refresh':
  458.                     refreshAction = a;
  459.                     break;
  460.             }
  461.             if (refreshAction) {
  462.                 // break queue processing here and wait for refresh to finish
  463.                 // before processing further operations
  464.                 break;
  465.             }
  466.         }
  467.         if (writeICS) {
  468.             if (refreshAction) {
  469.                 // reschedule the refresh for next round, after the file has been written;
  470.                 // strictly we may not need to refresh once the file has been successfully
  471.                 // written, but we don't know if that write will succeed.
  472.                 this.queue.unshift(refreshAction);
  473.             }
  474.             this.writeICS();
  475.         }
  476.         else if (refreshAction) {
  477.             this.doRefresh();
  478.         }
  479.     },
  480.  
  481.     lock: function () {
  482.         this.locked = true;
  483.     },
  484.  
  485.     unlock: function () {
  486.         this.locked = false;
  487.         this.processQueue();
  488.     },
  489.     
  490.     isLocked: function () {
  491.         return this.locked;
  492.     },
  493.  
  494.     startBatch: function ()
  495.     {
  496.         this.mObserver.onStartBatch();
  497.     },
  498.     endBatch: function ()
  499.     {
  500.         this.mObserver.onEndBatch();
  501.     },
  502.  
  503.     // nsIInterfaceRequestor impl
  504.     getInterface: function(iid, instance) {
  505.         if (iid.equals(Components.interfaces.nsIAuthPrompt)) {
  506.             return new calAuthPrompt();
  507.         }
  508.         else if (iid.equals(Components.interfaces.nsIPrompt)) {
  509.             // use the window watcher service to get a nsIPrompt impl
  510.             return Components.classes["@mozilla.org/embedcomp/window-watcher;1"]
  511.                              .getService(Components.interfaces.nsIWindowWatcher)
  512.                              .getNewPrompter(null);
  513.         }
  514.         Components.returnCode = Components.results.NS_ERROR_NO_INTERFACE;
  515.         return null;
  516.     },
  517.  
  518.     /**
  519.      * Make a backup of the (remote) calendar
  520.      *
  521.      * This will download the remote file into the profile dir.
  522.      * It should be called before every upload, so every change can be
  523.      * restored. By default, it will keep 3 backups. It also keeps one
  524.      * file each day, for 3 days. That way, even if the user doesn't notice
  525.      * the remote calendar has become corrupted, he will still loose max 1
  526.      * day of work.
  527.      * After the back up is finished, will call aCallback.
  528.      *
  529.      * @param aCallback
  530.      *           Function that will be calles after the backup is finished.
  531.      *           will be called in the original context in which makeBackup
  532.      *           was called
  533.      */
  534.     makeBackup: function(aCallback) {
  535.         // Uses |pseudoID|, an id of the calendar, defined below
  536.         function makeName(type) {
  537.             return 'calBackupData_'+pseudoID+'_'+type+'.ics';
  538.         }
  539.         
  540.         // This is a bit messy. createUnique creates an empty file,
  541.         // but we don't use that file. All we want is a filename, to be used
  542.         // in the call to copyTo later. So we create a file, get the filename,
  543.         // and never use the file again, but write over it.
  544.         // Using createUnique anyway, because I don't feel like 
  545.         // re-implementing it
  546.         function makeDailyFileName() {
  547.             var dailyBackupFile = backupDir.clone();
  548.             dailyBackupFile.append(makeName('day'));
  549.             dailyBackupFile.createUnique(CI.nsIFile.NORMAL_FILE_TYPE, 0600);
  550.             dailyBackupFileName = dailyBackupFile.leafName;
  551.  
  552.             // Remove the reference to the nsIFile, because we need to
  553.             // write over the file later, and you never know what happens
  554.             // if something still has a reference.
  555.             // Also makes it explicit that we don't need the file itself,
  556.             // just the name.
  557.             dailyBackupFile = null;
  558.  
  559.             return dailyBackupFileName;
  560.         }
  561.  
  562.         function purgeBackupsByType(files, type) {
  563.             // filter out backups of the type we care about.
  564.             var filteredFiles = files.filter(
  565.                 function f(v) { 
  566.                     return (v.name.indexOf("calBackupData_"+pseudoID+"_"+type) != -1)
  567.                 });
  568.             // Sort by lastmodifed
  569.             filteredFiles.sort(
  570.                 function s(a,b) {
  571.                     return (a.lastmodified - b.lastmodified);
  572.                 });
  573.             // And delete the oldest files, and keep the desired number of
  574.             // old backups
  575.             var i;
  576.             for (i = 0; i < filteredFiles.length - numBackupFiles; ++i) {
  577.                 file = backupDir.clone();
  578.                 file.append(filteredFiles[i].name);
  579.  
  580.                 // This can fail because of some crappy code in nsILocalFile.
  581.                 // That's not the end of the world.  We can try to remove the
  582.                 // file the next time around.
  583.                 try {
  584.                     file.remove(false);
  585.                 } catch(ex) {}
  586.             }
  587.             return;
  588.         }
  589.  
  590.         function purgeOldBackups() {
  591.             // Enumerate files in the backupdir for expiry of old backups
  592.             var dirEnum = backupDir.directoryEntries;
  593.             var files = [];
  594.             while (dirEnum.hasMoreElements()) {
  595.                 var file = dirEnum.getNext().QueryInterface(CI.nsIFile);
  596.                 if (file.isFile()) {
  597.                     files.push({name: file.leafName, lastmodified: file.lastModifiedTime});
  598.                 }
  599.             }
  600.  
  601.             if (doDailyBackup)
  602.                 purgeBackupsByType(files, 'day');
  603.             else
  604.                 purgeBackupsByType(files, 'edit');
  605.  
  606.             return;
  607.         }
  608.         
  609.         function copyToOverwriting(oldFile, newParentDir, newName) {
  610.             try {
  611.                 var newFile = newParentDir.clone();
  612.                 newFile.append(newName);
  613.             
  614.                 if (newFile.exists()) {
  615.                     newFile.remove(false);
  616.                 }
  617.                 oldFile.copyTo(newParentDir, newName);
  618.             } catch(e) {
  619.                 Components.utils.reportError("Backup failed, no copy:"+e);
  620.                 // Error in making a daily/initial backup.
  621.                 // not fatal, so just continue
  622.             }
  623.         }
  624.  
  625.         function getIntPrefSafe(prefName, defaultValue)
  626.         {
  627.             try {
  628.                 var prefValue = backupBranch.getIntPref(prefName);
  629.                 return prefValue;
  630.             }
  631.             catch (ex) {
  632.                 return defaultValue;
  633.             }
  634.         }
  635.         var backupDays = getIntPrefSafe("days", 1);
  636.         var numBackupFiles = getIntPrefSafe("filenum", 3);
  637.  
  638.         try {
  639.             var dirService = Components.classes["@mozilla.org/file/directory_service;1"]
  640.                                        .getService(CI.nsIProperties);
  641. // xxx todo: would we want to migrate the backups into getCalendarDirectory()?
  642.             var backupDir = dirService.get("ProfD", CI.nsILocalFile);
  643.             backupDir.append("backupData");
  644.             if (!backupDir.exists()) {
  645.                 backupDir.create(CI.nsIFile.DIRECTORY_TYPE, 0755);
  646.             }
  647.         } catch(e) {
  648.             // Backup dir wasn't found. Likely because we are running in
  649.             // xpcshell. Don't die, but continue the upload.
  650.             LOG("Backup failed, no backupdir:"+e);
  651.             aCallback.call(this);
  652.             return;
  653.         }
  654.  
  655.         try {
  656.             var pseudoID = this.getProperty("uniquenum");
  657.             if (!pseudoID) {
  658.                 pseudoID = new Date().getTime();
  659.                 this.setProperty("uniquenum", pseudoID);
  660.             }
  661.         } catch(e) {
  662.             // calendarmgr not found. Likely because we are running in
  663.             // xpcshell. Don't die, but continue the upload.
  664.             LOG("Backup failed, no calendarmanager:"+e);
  665.             aCallback.call(this);
  666.             return;
  667.         }
  668.  
  669.         var doInitialBackup = false;
  670.         var initialBackupFile = backupDir.clone();
  671.         initialBackupFile.append(makeName('initial'));
  672.         if (!initialBackupFile.exists())
  673.             doInitialBackup = true;
  674.  
  675.         var doDailyBackup = false;
  676.         var backupTime = this.getProperty('backup-time');
  677.         if (!backupTime ||
  678.             (new Date().getTime() > backupTime + backupDays*24*60*60*1000)) {
  679.             // It's time do to a daily backup
  680.             doDailyBackup = true;
  681.             this.setProperty('backup-time', new Date().getTime());
  682.         }
  683.  
  684.         var dailyBackupFileName;
  685.         if (doDailyBackup) {
  686.             dailyBackupFileName = makeDailyFileName(backupDir);
  687.         }
  688.  
  689.         var backupFile = backupDir.clone();
  690.         backupFile.append(makeName('edit'));
  691.         backupFile.createUnique(CI.nsIFile.NORMAL_FILE_TYPE, 0600);
  692.         
  693.         purgeOldBackups();
  694.  
  695.         // Now go download the remote file, and store it somewhere local.
  696.         var ioService = Components.classes["@mozilla.org/network/io-service;1"]
  697.                                   .getService(CI.nsIIOService);
  698.         var channel = ioService.newChannelFromURI(this.mUri);
  699.         channel.loadFlags |= Components.interfaces.nsIRequest.LOAD_BYPASS_CACHE;
  700.         channel.notificationCallbacks = this;
  701.  
  702.         var downloader = Components.classes["@mozilla.org/network/downloader;1"]
  703.                                    .createInstance(CI.nsIDownloader);
  704.  
  705.         var savedthis = this;
  706.         var listener = {
  707.             onDownloadComplete: function(downloader, request, ctxt, status, result) {
  708.                 if (doInitialBackup)
  709.                     copyToOverwriting(result, backupDir, makeName('initial'));
  710.                 if (doDailyBackup)
  711.                     copyToOverwriting(result, backupDir, dailyBackupFileName);
  712.  
  713.                 aCallback.call(savedthis);
  714.             }
  715.         }
  716.  
  717.         downloader.init(listener, backupFile);
  718.         try {
  719.             channel.asyncOpen(downloader, null);
  720.         } catch(e) {
  721.             // For local files, asyncOpen throws on new (calendar) files
  722.             // No problem, go and upload something
  723.             LOG("Backup failed in asyncOpen:"+e);
  724.             aCallback.call(this);
  725.             return;
  726.         }
  727.  
  728.         return;
  729.     }
  730. };
  731.  
  732. function calICSObserver(aCalendar) {
  733.     this.mCalendar = aCalendar;
  734. }
  735.  
  736. calICSObserver.prototype = {
  737.     mCalendar: null,
  738.     mInBatch: false,
  739.  
  740.     // calIObserver:
  741.     onStartBatch: function() {
  742.         this.mCalendar.observers.notify("onStartBatch");
  743.         this.mInBatch = true;
  744.     },
  745.     onEndBatch: function() {
  746.         this.mCalendar.observers.notify("onEndBatch");
  747.         this.mInBatch = false;
  748.     },
  749.     onLoad: function(calendar) {
  750.         this.mCalendar.observers.notify("onLoad", [calendar]);
  751.     },
  752.     onAddItem: function(aItem) {
  753.         this.mCalendar.observers.notify("onAddItem", [aItem]);
  754.     },
  755.     onModifyItem: function(aNewItem, aOldItem) {
  756.         this.mCalendar.observers.notify("onModifyItem", [aNewItem, aOldItem]);
  757.     },
  758.     onDeleteItem: function(aDeletedItem) {
  759.         this.mCalendar.observers.notify("onDeleteItem", [aDeletedItem]);
  760.     },
  761.     onPropertyChanged: function(aCalendar, aName, aValue, aOldValue) {
  762.         this.mCalendar.observers.notify("onPropertyChanged", [aCalendar, aName, aValue, aOldValue]);
  763.     },
  764.     onPropertyDeleting: function(aCalendar, aName) {
  765.         this.mCalendar.observers.notify("onPropertyDeleting", [aCalendar, aName]);
  766.     },
  767.  
  768.     // Unless an error number is in this array, we consider it very bad, set
  769.     // the calendar to readOnly, and give up.
  770.     acceptableErrorNums: [],
  771.  
  772.     onError: function(aErrNo, aMessage) {
  773.         var errorIsOk = false;
  774.         for each (num in this.acceptableErrorNums) {
  775.             if (num == aErrNo) {
  776.                 errorIsOk = true;
  777.                 break;
  778.             }
  779.         }
  780.         if (!errorIsOk)
  781.             this.mCalendar.readOnly = true;
  782.         this.mCalendar.observers.notify("onError", [aErrNo, aMessage]);
  783.     }
  784. };
  785.  
  786. /***************************
  787.  * Transport Abstraction Hooks
  788.  *
  789.  * Those hooks provide a way to do checks before or after publishing an
  790.  * ics file. The main use will be to check etags (or some other way to check
  791.  * for remote changes) to protect remote changes from being overwritten.
  792.  *
  793.  * Different protocols need different checks (webdav can do etag, but
  794.  * local files need last-modified stamps), hence different hooks for each
  795.  * types
  796.  */
  797.  
  798. // dummyHooks are for transport types that don't have hooks of their own.
  799. // Also serves as poor-mans interface definition.
  800. function dummyHooks() {
  801. }
  802.  
  803. dummyHooks.prototype = {
  804.     onBeforeGet: function(aChannel) {
  805.         return true;
  806.     },
  807.     
  808.     /**
  809.      * @return
  810.      *     a boolean, false if the previous data should be used (the datastore
  811.      *     didn't change, there might be no data in this GET), true in all
  812.      *     other cases
  813.      */
  814.     onAfterGet: function() {
  815.         return true;
  816.     },
  817.  
  818.     onBeforePut: function(aChannel) {
  819.         return true;
  820.     },
  821.     
  822.     onAfterPut: function(aChannel) {
  823.         return true;
  824.     }
  825. };
  826.  
  827. function httpHooks() {
  828.     this.mChannel = null;
  829. }
  830.  
  831. httpHooks.prototype = {
  832.     onBeforeGet: function(aChannel) {
  833.         this.mChannel = aChannel;
  834.         if (this.mEtag) {
  835.             var httpchannel = aChannel.QueryInterface(Components.interfaces.nsIHttpChannel);
  836.             // Somehow the webdav header 'If' doesn't work on apache when
  837.             // passing in a Not, so use the http version here.
  838.             httpchannel.setRequestHeader("If-None-Match", this.mEtag, false);
  839.         }
  840.  
  841.         return true;
  842.     },
  843.     
  844.     onAfterGet: function() {
  845.         var httpchannel = this.mChannel.QueryInterface(Components.interfaces.nsIHttpChannel);
  846.  
  847.         // 304: Not Modified
  848.         // Can use the old data, so tell the caller that it can skip parsing.
  849.         if (httpchannel.responseStatus == 304)
  850.             return false;
  851.  
  852.         // 404: Not Found
  853.         // This is a new calendar. Shouldn't try to parse it. But it also
  854.         // isn't a failure, so don't throw.
  855.         if (httpchannel.responseStatus == 404)
  856.             return false;
  857.  
  858.         try {
  859.             this.mEtag = httpchannel.getResponseHeader("ETag");
  860.         } catch(e) {
  861.             // No etag header. Now what?
  862.             this.mEtag = null;
  863.         }
  864.         this.mChannel = null;
  865.         return true;
  866.     },
  867.  
  868.     onBeforePut: function(aChannel) {
  869.         if (this.mEtag) {
  870.             var httpchannel = aChannel.QueryInterface(Components.interfaces.nsIHttpChannel);
  871.  
  872.             // Apache doesn't work correctly with if-match on a PUT method,
  873.             // so use the webdav header
  874.             httpchannel.setRequestHeader("If", '(['+this.mEtag+'])', false);
  875.         }
  876.         return true;
  877.     },
  878.     
  879.     onAfterPut: function(aChannel) {
  880.         var httpchannel = aChannel.QueryInterface(Components.interfaces.nsIHttpChannel);
  881.         try {
  882.             this.mEtag = httpchannel.getResponseHeader("ETag");
  883.         } catch(e) {
  884.             // There was no ETag header on the response. This means that
  885.             // putting is not atomic. This is bad. Race conditions can happen,
  886.             // because there is a time in which we don't know the right
  887.             // etag.
  888.             // Try to do the best we can, by immediatly getting the etag.
  889.             
  890.             // Only on branch, because webdav doesn't work on trunk: bug 332840
  891.             if (isOnBranch) {
  892.                 var res = new WebDavResource(aChannel.URI);
  893.                 var webSvc = Components.classes['@mozilla.org/webdav/service;1']
  894.                                        .getService(Components.interfaces.nsIWebDAVService);
  895.                 // The namespace is 'DAV:', not just 'DAV'.
  896.                 webSvc.getResourceProperties(res, 1, ['DAV: getetag'], false,
  897.                                              this, null, null);
  898.             } else {
  899.                 // instead, on trunk, set mEtag to null, so it will be ignored on 
  900.                 // the next GET/PUT
  901.                 this.mEtag = null;
  902.             }
  903.         }
  904.         return true;
  905.     },
  906.  
  907.     onOperationComplete: function(aStatusCode, aResource, aOperation, aClosure) {
  908.     },
  909.  
  910.     onOperationDetail: function(aStatusCode, aResource, aOperation, aDetail, aClosure) {
  911.         var props = aDetail.QueryInterface(Components.interfaces.nsIProperties);
  912.         try {
  913.             this.mEtag = props.get('DAV: getetag', Components.interfaces.nsISupportsString).toString();
  914.         } catch(e) {
  915.             // No etag header. Now what?
  916.             this.mEtag = null;
  917.         }
  918.     }
  919. };
  920.  
  921. function WebDavResource(url) {
  922.     this.mResourceURL = url;
  923. }
  924.  
  925. WebDavResource.prototype = {
  926.     mResourceURL: {},
  927.     get resourceURL() { return this.mResourceURL; },
  928.     QueryInterface: function(iid) {
  929.         if (iid.equals(CI.nsIWebDAVResource) ||
  930.             iid.equals(CI.nsISupports)) {
  931.             return this;
  932.         }
  933.         throw Components.interfaces.NS_ERROR_NO_INTERFACE;
  934.     }
  935. };
  936.